/*
 * Copyright (C) 2011, EADS France
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 */
package xowl.gmi.environment;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import xowl.utils.config.Configuration;

/**
 *
 * @author Laurent WOUTERS
 */
public class MainController implements xowl.gmi.view.MainView.Actions {
    final static String name = "GMI";
    
    private static final String optionsFile = "options.ini";
    private static final String workbenchesDir = "workbenches";
    private static final String workbenchFN = "workbench.ini";
    
    private static MainController instance;
    
    public static MainController create(org.apache.log4j.Level loglevel) {
        if (instance != null)
            return instance;
        instance = new MainController(loglevel);
        LoadingSplash loader = new LoadingSplash(instance.properties, new LongJob() {
            @Override public String getMessage() { return instance.locale.initMessage + "\n"; }
            @Override public boolean canCancel() { return false; }
            @Override public void requestCancel() { }
            @Override public Object run(LongJobHandler handler) {
                instance.initDeploySafetyNet();
                handler.output(instance.locale.initLoadConfig + "\n");
                instance.initLoadConfig();
                handler.output(instance.locale.initInterpreter + "\n");
                instance.initXOWL();
                handler.output(instance.locale.initWorkbench + "\n");
                instance.initWorkbench();
                Main.getApplication().show(instance.getView());
                return null;
            }
        });
        loader.setVisible(true);
        return instance;
    }
    
    private xowl.utils.logging.Logger log;
    private Properties properties;
    private Options options;
    private Map<String, Locale> locales;
    private Locale locale;
    private Hooks hooks;
    private xowl.gmi.model.Workbench workbench;
    private xowl.interpreter.Repository repository;
    private xowl.gmi.view.MainView view;
    private List<UIAction> uiActions;
    private int lastUIAction;
    private List<ProjectController> projects;
    private List<DiagramController> openDiagrams;
    private Map<xowl.interpreter.Thread, ProjectExecution> runnings;
    private xowl.lang.owl2.IRI clipboard;
    
    public xowl.utils.logging.Logger getLog() { return log; }
    public Properties getProperties() { return properties; }
    public Options getOptions() { return options; }
    public Locale getLocale() { return locale; }
    public Collection<xowl.interpreter.Hook> getHooks() { return hooks.get(); }
    public xowl.gmi.model.Workbench getWorkbench() { return workbench; }
    public xowl.interpreter.Repository getRepository() { return repository; }
    public xowl.gmi.view.MainView getView() { return view; }
    
    private MainController(org.apache.log4j.Level loglevel) {
        log = new xowl.utils.logging.Logger(MainController.class.getName(), loglevel);
        try {
            java.io.InputStream stream = getResourceStream("xowl/gmi/resources/properties.ini");
            Configuration conf = new Configuration();
            conf.load(stream, Charset.forName("UTF-8"));
            properties = new Properties(conf);
        } catch (java.io.IOException ex) {
            log.fatal(name, "Failed to load property resource!", ex);
        }
        locales = new HashMap<>();
        for (String id : properties.locale)
            initLoadLocale(id);
        locale = locales.get(getDefaultLocale());
        uiActions = new ArrayList<>();
        projects = new ArrayList<>();
        openDiagrams = new ArrayList<>();
        runnings = new HashMap<>();
        lastUIAction = -1;
        xowl.interpreter.Interpreter.config(log, properties.libs);
    }
    private void initDeploySafetyNet() {
        java.lang.Thread.setDefaultUncaughtExceptionHandler(new java.lang.Thread.UncaughtExceptionHandler() {
            @Override public void uncaughtException(java.lang.Thread thread, java.lang.Throwable throwable) { onUncaughtException(thread, throwable); }
        });
    }
    private void initLoadLocale(String id) {
        Configuration loc = new Configuration();
        try {
            java.io.InputStream stream = getResourceStream("xowl/gmi/resources/locales/" + id + ".ini");
            loc.load(stream, Charset.forName("UTF-8"));
            locales.put(id, new Locale(loc));
        } catch (java.io.IOException ex) {
            log.fatal(name, "Failed to load locale " + id, ex);
        }
    }
    private void initLoadConfig() {
        // Load options file
        if ((new File(optionsFile)).exists()) {
            try {
                Configuration conf = new Configuration();
                conf.load(optionsFile);
                options = new Options(conf);
            }
            catch (java.io.IOException ex) {
                log.fatal(name, "Failed to load options file: " + optionsFile, ex);
                return;
            }
        } else {
            initAskOptions();
        }
        try { workbench = new xowl.gmi.model.Workbench(options.workbench); }
        catch (java.io.IOException ex) {
            log.fatal(name, "Failed to load workbench file: " + options.workbench, ex);
            return;
        }
        locale = locales.get(options.locale);
    }
    private void initAskOptions() {
        xowl.gmi.view.DialogOptions win = new xowl.gmi.view.DialogOptions(locale);
        win.searchWorkbenches(workbenchesDir, workbenchFN);
        for (String loc : locales.keySet())
            win.addLocale(loc, locales.get(loc).meta_name, locales.get(loc).meta_icon);
        
        win.prepare(getDefaultLocale());
        win.setVisible(true);
        options = new Options();
        options.workbench = win.getSelectedWorkbench();
        options.locale = win.getSelectedLocale();
        try {
            Configuration conf = new Configuration();
            options.save(conf);
            conf.save(optionsFile, java.nio.charset.Charset.forName("UTF-8"));
        }
        catch (java.io.IOException ex) { log.fatal(name, "Failed to save options file: " + optionsFile, ex); }
    }
    private void initXOWL() {
        repository = xowl.interpreter.Interpreter.createRepositoryNativeRDF(log);
        hooks = new Hooks(this, repository);
    }
    private void initWorkbench() {
        // Load runtime libraries and patches
        loadResource("lang/CVSSL.owl", xowl.interpreter.OWL2Syntax.Functional);
        loadResource("lang/AST.owl", xowl.interpreter.OWL2Syntax.Functional);
        loadResource("libs/indexer.owl", xowl.interpreter.OWL2Syntax.Functional);
        loadResource("libs/utils.array.owl", xowl.interpreter.OWL2Syntax.Functional);
        loadResource("libs/utils.java.owl", xowl.interpreter.OWL2Syntax.Functional);
        loadResource("libs/utils.map.owl", xowl.interpreter.OWL2Syntax.Functional);
        loadResource("libs/utils.stack.owl", xowl.interpreter.OWL2Syntax.Functional);
        // Load the current workbench
        this.workbench.load(repository);
        this.view = new xowl.gmi.view.MainView(Main.getApplication(), properties, locale, this);
        for (xowl.gmi.model.ExecutionEntry entry : workbench.getExecEntries())
            view.addExecutionChoice(entry.getID(), entry.getName());
        this.view.setUndoStatus(null);
        this.view.setRedoStatus(null);
    }
    private void loadResource(String resource, xowl.interpreter.OWL2Syntax syntax) {
        try (java.io.InputStream stream = getResourceStream("xowl/gmi/runtime/" + resource)) {
            repository.loadOntology(resource, stream, syntax);
            stream.close();
        } catch (java.io.IOException ex) { log.error(name, "Error while loading resource " + resource, ex); }
    }
    private java.io.InputStream getResourceStream(String resource) throws java.io.IOException {
        java.lang.ClassLoader loader = java.lang.ClassLoader.getSystemClassLoader();
        return loader.getResourceAsStream(resource);
    }
    private String getDefaultLocale() {
        String defID = java.util.Locale.getDefault().getLanguage() + "-" + java.util.Locale.getDefault().getCountry();
        if (locales.containsKey(defID))
            return defID;
        return properties.locale_fallback;
    }
    
    
    public void registerExecution(xowl.interpreter.Thread thread, ProjectExecution exe) { runnings.put(thread, exe); }
    public void unregisterExecution(xowl.interpreter.Thread thread) { runnings.remove(thread); }
    public ProjectExecution getExecution(xowl.interpreter.Thread thread) { return runnings.get(thread); }
    
    public void removeProject(ProjectController project) {
        view.getWinProjects().removeProjectNode(project.getView());
        projects.remove(project);
    }
    public void openDiagram(DiagramController diagram) {
        openDiagrams.add(diagram);
        view.addOpenTab(diagram.getTitle(), diagram.getViewContent());
    }
    public void closeDiagram(DiagramController diagram) {
        openDiagrams.remove(diagram);
        view.removeOpenTab(diagram.getViewContent());
    }
    
    public void executeUIAction(UIAction action) {
        action.execute();
        while (lastUIAction != uiActions.size() - 1)
            uiActions.remove(uiActions.size() - 1);
        uiActions.add(action);
        lastUIAction++;
        view.setUndoStatus(action.getDescription());
        view.setRedoStatus(null);
        view.getWinOutput().repaint();
    }
    
    public xowl.lang.owl2.IRI getClipboard() { return clipboard; }
    public void setClipboard(xowl.lang.owl2.IRI iri) { clipboard = iri ;}
    
    public int showConfirm(String question, boolean canCancel) {
        int option = javax.swing.JOptionPane.YES_NO_OPTION;
        if (canCancel)
            option = javax.swing.JOptionPane.YES_NO_CANCEL_OPTION;
        return javax.swing.JOptionPane.showConfirmDialog(view.getComponent(), question, properties.title, option, javax.swing.JOptionPane.QUESTION_MESSAGE);
    }
    
    public boolean canExit() {
        boolean modifications = false;
        for (ProjectController project : projects)
            modifications = modifications || project.isModified();
        if (modifications) {
            int res = showConfirm(locale.appOnClose, true);
            if (res == javax.swing.JOptionPane.CANCEL_OPTION)
                return false;
            if (res == javax.swing.JOptionPane.YES_OPTION) {
                for (ProjectController project : projects) {
                    if (project.isModified())
                        project.onSave();
                }
            }
        }
        return true;
    }
    
    public void onExit() { }
    
    private void onUncaughtException(java.lang.Thread thread, final java.lang.Throwable throwable) {
        xowl.gmi.view.DialogOnException win = new xowl.gmi.view.DialogOnException(new xowl.gmi.view.DialogOnException.Actions() {
            @Override public void reportEmail(boolean config, boolean workbench) {
                browseTo(generateMail(throwable, config));
            }
            @Override public void reportOnline(boolean config, boolean workbench) {
                browseTo(properties.bugreport_site);
            }
        }, locale, view.getFrame(), serialize(throwable));
        win.setVisible(true);
    }
    
    @Override public void onFileNewProject() {
        String input = javax.swing.JOptionPane.showInputDialog(view.getComponent(), locale.appNewProject, properties.title, javax.swing.JOptionPane.QUESTION_MESSAGE);
        if (input == null || input.isEmpty())
            return;
        ProjectController project = new ProjectController(this, input);
        projects.add(project);
        view.getWinProjects().addProjectNode(project.getView());
    }
    @Override public void onFileNewDiagram() {
        if (projects.isEmpty())
            return;
        xowl.gmi.view.DialogNewDiagram dialog = new xowl.gmi.view.DialogNewDiagram(locale, view.getFrame(), true);
        for (ProjectController pc : projects)
            dialog.addProject(pc, pc.getModel().getName());
        for (xowl.gmi.model.WorkbenchView wbv : workbench.getViews())
            dialog.addWBView(wbv, wbv.getName());
        dialog.prepare();
        dialog.setVisible(true);
        if (!dialog.isAccept())
            return;
        ((ProjectController)dialog.getSelectedProject()).addDiagram(dialog.getSelectedName(), (xowl.gmi.model.WorkbenchView)dialog.getSelectedView());
    }
    @Override public void onFileOpenProject() {
        javax.swing.JFileChooser chooser = new javax.swing.JFileChooser();
        chooser.setCurrentDirectory(new java.io.File("workspace"));
        chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("PROJECT File", "project"));
        int value = chooser.showOpenDialog(null);
        if (value != javax.swing.JFileChooser.APPROVE_OPTION)
            return;
        String file = chooser.getSelectedFile().getAbsolutePath();
        Configuration config = new Configuration();
        try { config.load(file, Charset.forName("UTF-8")); }
        catch (java.io.IOException ex) {
            log.fatal(name, "Failed to load project file: " + file, ex);
            return;
        }
        xowl.gmi.model.Project pm = new xowl.gmi.model.Project(config, workbench);
        ProjectController project = new ProjectController(this, pm, file);
        projects.add(project);
        view.getWinProjects().addProjectNode(project.getView());
    }
    @Override public void onFileCloseProject(xowl.gmi.view.ProjectNode pv) { getProjectFor(pv).onClose(); }
    @Override public void onFileExport(xowl.gmi.view.ProjectNode pv, xowl.gmi.view.DiagramView dv) {
        xowl.gmi.view.DialogExport.Actions actions = new xowl.gmi.view.DialogExport.Actions() {
            @Override public LongJob getJob(final String location, int format, final List<Object> selected) {
                ExportFormat temp = ExportFormat.png;
                if (format == xowl.gmi.view.DialogExport.formatBMP) temp = ExportFormat.bmp;
                else if (format == xowl.gmi.view.DialogExport.formatSVG) temp = ExportFormat.svg;
                final ExportFormat ef = temp;
                return new LongJob() {
                    private boolean onCancel = false;
                    @Override public String getMessage() { return locale.exportDlgTitle; }
                    @Override public boolean canCancel() { return true; }
                    @Override public void requestCancel() { onCancel = true; }
                    @Override public Object run(LongJobHandler handler) {
                        int count = 1;
                        for (Object obj : selected) {
                            if (onCancel) return true;
                            DiagramController diagram = (DiagramController)obj;
                            handler.output(diagram.getTitle());
                            try { diagram.export(location, ef); }
                            catch (java.io.IOException e) {
                                log.error(name, "Failed to export diagram: " + diagram.getTitle(), e);
                            }
                            handler.setAdvance(count * 100 / selected.size());
                            count++;
                        }
                        return false;
                    }
                };
            }
        };
        xowl.gmi.view.DialogExport dialog = new xowl.gmi.view.DialogExport(actions, locale, view.getFrame());
        for (ProjectController project : projects) {
            for (DiagramController diagram : project.getDiagrams()) {
                dialog.addDiagram(diagram, project.toString() + " - " + diagram.getTitle());
            }
        }
        dialog.setVisible(true);
    }
    @Override public void onFileSave(xowl.gmi.view.ProjectNode pv) { getProjectFor(pv).onSave(); }
    @Override public void onFileSaveAs(xowl.gmi.view.ProjectNode pv) { getProjectFor(pv).onSaveAs(); }
    @Override public void onFileSaveAll() {
        for (ProjectController project : projects)
            project.onSave();
    }
    
    @Override public void onEditUndo() {
        if (lastUIAction == -1)
            return;
        uiActions.get(lastUIAction).undo();
        view.setRedoStatus(uiActions.get(lastUIAction).getDescription());
        lastUIAction--;
        if (lastUIAction != -1)
            view.setUndoStatus(uiActions.get(lastUIAction).getDescription());
        else
            view.setUndoStatus(null);
        view.getWinOutput().repaint();
    }
    @Override public void onEditRedo() {
        if (lastUIAction + 1 < uiActions.size()) {
            lastUIAction++;
            uiActions.get(lastUIAction).redo();
            view.setUndoStatus(uiActions.get(lastUIAction).getDescription());
            if (lastUIAction + 1 < uiActions.size())
                view.setRedoStatus(uiActions.get(lastUIAction).getDescription());
            else
                view.setRedoStatus(null);
            view.getWinOutput().repaint();
        }
    }
    @Override public void onEditCut(xowl.gmi.view.DiagramView dv) { getDiagramFor(dv).onEditCut(); }
    @Override public void onEditCopy(xowl.gmi.view.DiagramView dv) { getDiagramFor(dv).onEditCopy(); }
    @Override public void onEditPaste(xowl.gmi.view.DiagramView dv) { getDiagramFor(dv).onEditPaste(); }
    @Override public void onEditRemove(xowl.gmi.view.DiagramView dv) { getDiagramFor(dv).onEditRemove(); }
    @Override public void onEditDelete(xowl.gmi.view.DiagramView dv) { getDiagramFor(dv).onEditDelete(); }

    @Override public void onRunExecute(xowl.gmi.view.ProjectNode pv, String id) { getProjectFor(pv).onExecute(id); }
    
    @Override public void onHelpBug() { browseTo(properties.bugreport_site); }
    
    private ProjectController getProjectFor(xowl.gmi.view.ProjectNode pv) {
        for (ProjectController project : projects)
            if (project.getView() == pv)
                return project;
        return null;
    }
    
    private DiagramController getDiagramFor(xowl.gmi.view.DiagramView dv) {
        for (DiagramController diagram : openDiagrams)
            if (diagram.getViewContent() == dv)
                return diagram;
        return null;
    }
    
    private void browseTo(String uri) {
        if (!java.awt.Desktop.isDesktopSupported())
            return;
        java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
        if (!desktop.isSupported(java.awt.Desktop.Action.BROWSE))
            return;
        try { desktop.browse(new java.net.URI(uri)); }
        catch (URISyntaxException | IOException ex) { }
    }
    
    private String generateMail(java.lang.Throwable throwable, boolean includeSystem) {
        StringBuilder builder = new StringBuilder("mailto:");
        builder.append(properties.bugreport_email);
        builder.append("?subject=Bug%20in%20XOWL%20GMI%20Environment%20v");
        builder.append(properties.version);
        //builder.append("&body=");
        //generateMessage(builder, throwable, includeSystem);
        return builder.toString();
    }
    
    private String generateMessage(java.lang.StringBuilder builder, java.lang.Throwable throwable, boolean includeSystem) {
        builder.append("Hello,\n");
        builder.append("While doing the following:\n");
        builder.append("<explain here>\n\n");
        builder.append("I had the following error:\n");
        serialize(throwable, builder);
        builder.append("\n\n");
        if (includeSystem) {
            
        }
        builder.append("Best regards,\n");
        return builder.toString();
    }
    
    private String serialize(java.lang.Throwable ex) {
        java.lang.StringBuilder builder = new java.lang.StringBuilder(ex.toString());
        serialize(ex, builder);
        return builder.toString();
    }

    private void serialize(java.lang.Throwable ex, java.lang.StringBuilder builder) {
        builder.append(ex.toString());
        builder.append("\n");
        for (java.lang.StackTraceElement element : ex.getStackTrace()) {
            builder.append("    ");
            builder.append(element.toString());
            builder.append("\n");
        }
        java.lang.Throwable cause = ex.getCause();
        if (cause != null) {
            builder.append("Caused by:\n");
            serialize(cause, builder);
        }
    }
}
